/*
 *
 * Copyright (c) 2023 - future
 * @author fzh
 * @email e_341877120_mail@163.com
 * @link https://mvnrepository.com/search?q=io.github.xrfzh.cn
 *
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package cn.xrfzh.common.aop;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.xrfzh.common.annotation.SystemLog;
import cn.xrfzh.common.constants.RedisConstant;
import cn.xrfzh.common.entity.ExceptionLog;
import cn.xrfzh.common.entity.Log;
import cn.xrfzh.common.entity.OperationLog;
import cn.xrfzh.common.entity.SimpleUser;
import cn.xrfzh.common.redis.RedisCacheService;
import cn.xrfzh.common.sender.LogSender;
import cn.xrfzh.common.utils.IpUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;

/**
 * 定义日志切面类
 */
@Slf4j
@Order(1)
@Aspect
@Component
public class SystemLogAspect {

	@Resource
	private LogSender logSender;

	@Resource
	private RedisCacheService redisCacheService;

	private String getUsername() {
		return getSimpleUser().getUserName() != null ? getSimpleUser().getUserName() : "admin";
	}

	private Long getUserId() {
		return getSimpleUser().getUserId() != null ? getSimpleUser().getUserId() : 0L;
	}

	private SimpleUser getSimpleUser() {
		String simpleUserJsonStr = redisCacheService.get(RedisConstant.CURRENT_LOGIN_USER_KEY);
		return JSONUtil.toBean(simpleUserJsonStr, SimpleUser.class);
	}

	/**
	 * 设置操作日志切入点，在注解的位置切入代码
	 * @param systemLog 日志注解
	 */
	@Pointcut("@annotation(systemLog)")
	public void operationLogPointCut(SystemLog systemLog) {
	}

	/**
	 * 设置异常日志切入点，扫描所有controller包下操作
	 */
	@Pointcut("execution(* com..*.controller..*.*(..))")
	public void exceptionLogPointCut() {
	}

	/**
	 * 操作日志环绕通知
	 * @param joinPoint 切入点
	 * @param responseData 响应数据
	 * @param systemLog 日志注解
	 */
	@AfterReturning(value = "operationLogPointCut(systemLog)", returning = "responseData",
			argNames = "joinPoint,responseData,systemLog")
	public void doAround(JoinPoint joinPoint, Object responseData, SystemLog systemLog) {
		OperationLog operationLog = new OperationLog();
		operationLog.setRespContent(StrUtil.maxLength(JSONUtil.toJsonStr(responseData), 10000));
		operationLog.setUserId(getUserId());
		operationLog.setUserName(getUsername());
		operationLog.setIp(IpUtil.getIp());
		// 封装公共日志信息
		setRequestParamsAndMethodName(operationLog, joinPoint);
		operationLog.setModule(systemLog.module());
		operationLog.setType(systemLog.type().toString());
		operationLog.setDescription(systemLog.description());
		logSender.sendOperationLog(operationLog);
	}

	/**
	 * 异常返回通知，用于拦截异常日志信息，连接点抛出异常后执行
	 * @param joinPoint 切入点
	 * @param exception 异常信息
	 */
	@AfterThrowing(pointcut = "exceptionLogPointCut()", throwing = "exception")
	public void saveExceptionLog(JoinPoint joinPoint, Throwable exception) {
		ExceptionLog exceptionLog = new ExceptionLog();
		exceptionLog.setUserId(getUserId());
		exceptionLog.setUserName(getUsername());
		exceptionLog.setIp(IpUtil.getIp());
		// 封装公共日志信息
		setRequestParamsAndMethodName(exceptionLog, joinPoint);
		exceptionLog.setName(exception.getClass().getName());
		exceptionLog.setMessage(StrUtil.maxLength(
				stackTraceToString(exception.getClass().getName(), exception.getMessage(), exception.getStackTrace()),
				10000));
		logSender.sendExceptionLog(exceptionLog);
	}

	/**
	 * 转换异常信息为字符串
	 * @param exceptionName 异常名称
	 * @param exceptionMessage 异常信息
	 * @param elements 堆栈信息
	 */
	public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
		StringBuilder sb = new StringBuilder();
		sb.append(exceptionName).append(":").append(exceptionMessage).append("\n\t");
		for (StackTraceElement stet : elements) {
			sb.append(stet).append("\n");
		}
		return sb.toString();
	}

	/**
	 * 封装公共日志信息
	 * @param log 公共日志对象
	 * @param joinPoint 切入点
	 */
	private void setRequestParamsAndMethodName(Log log, JoinPoint joinPoint) {
		RequestAttributes ra = RequestContextHolder.getRequestAttributes();
		ServletRequestAttributes sra = (ServletRequestAttributes) ra;
		assert sra != null;
		HttpServletRequest request = sra.getRequest();
		String httpMethod = request.getMethod();
		String queryString = request.getQueryString();
		Object[] args = joinPoint.getArgs();
		String params = "";
		if (args.length > 0) {
			if ("GET".equals(httpMethod) || "DELETE".equals(httpMethod)) {
				params = queryString;
			}
			else {
				for (int i = 0; i < args.length; i++) {
					if (args[i] instanceof ServletRequest) {
						args[i] = ((HttpServletRequest) args[i]).getParameterMap();
					}
					else if (args[i] instanceof ServletResponse) {
						args[i] = "ServletResponse对象";
					}
					else if (args[i] instanceof MultipartFile) {
						args[i] = "MultipartFile对象";
					}
					else if (args[i] instanceof MultipartFile[]) {
						args[i] = "MultipartFile数组";
					}
					else if (args[i] instanceof BindingResult) {
						args[i] = "BindingResult对象";
					}
				}
				params = JSONUtil.toJsonStr(args);
			}
		}
		// 从切面切入点处通过反射机制获取切入点处的方法
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		// 获取切入点所在的方法
		Method method = signature.getMethod();
		String className = joinPoint.getTarget().getClass().getName();
		String methodName = className + "." + method.getName();
		log.setRequestParam(StrUtil.maxLength(params, 10000));
		log.setMethod(methodName);
		log.setUri(request.getRequestURI());
	}

}
